#!/usr/bin/env ruby

=begin
Script: discovery_update
Author: Jean-Jacques Martrès (jjmartres |at| gmail |dot| com)
Description: Discovery update extend the discovery capabilities of Zabbix
License: GPL2

This script is intended for use with Zabbix > 2.0

USAGE:

  as a script:    discovery_update [options]

  OPTIONS
     -h, --help                            Display this help message
     -c, --config CONFIG_FILE              Configuration file

  CONFIG FILE FORMAT

     api:
       url:                "zabbix api url"
       login:              "zabbix username"
       password:           "zabbix password"

     your-proxy-name:
       group:              "host group name"
       snmp_community:     "device SNMP community"
       rules:              "[['TEMPLATE_NAME_1','regex1'],['TEMPLATE_NAME_2','regex2']]"
=end
require 'rubygems'
require 'optparse'
require 'rubix'
require 'snmp'
require 'logger'
require 'yaml'
require 'net/ping/external'

version="0.4.7"

# Howto use it ... quiet simple
OPTIONS = {}
mandatory_options=[:config]
optparse = OptionParser.new do |opts|
  opts.banner = "Usage: #{$0} [options]"
  opts.separator ""
  opts.separator "Options"
  opts.on("-h", "--help", "Display this help message") do
    puts opts
    exit(-1)
  end
  opts.on('-c', '--config CONFIG_FILE',String,  'Configuration file') { |v| OPTIONS[:config] = v }
  opts.separator ""
end


# Show usage when no args pass
if ARGV.empty?
  puts optparse
  exit(-1)
end

# Validate that mandatory parameters are specified
begin
  optparse.parse!(ARGV)
  missing = mandatory_options.select{|p| OPTIONS[p].nil? }
  if not missing.empty?
    puts "Missing options: #{missing.join(', ')}"
    puts optparse
    exit(-1)
  end
  rescue OptionParser::ParseError,OptionParser::InvalidArgument,OptionParser::InvalidOption
       puts $!.to_s
       exit(-1)
end

# Log each events
log = Logger.new(STDOUT)

# Start logger
log.level = Logger::DEBUG
log.debug ""
log.debug "Starting discovery update"
log.debug ""

# Read file config
if File.exist?(OPTIONS[:config])
 cnf = YAML::load(File.open(OPTIONS[:config]))
else
 log.error "File #{OPTIONS[:config]} doesn't exist !"
 exit(-1)
end

# Connect to the Zabbix API
Rubix.connect(cnf["api"]["url"],cnf["api"]["login"],cnf["api"]["password"])

# List all configuration
cnf.each_key { |key|
  if key !~ /api/
    log.info "Working on #{key}..."

    # We need to ensure that the provided proxy exist
    proxy = Rubix.connection.request('proxy.get', 'filter' => { 'host' => key })
    if proxy.has_data?
      proxy = proxy.result.reduce Hash.new, :merge
    else
      log.error "Proxy #{key} doesn't exist !" if proxy.success?
      exit(-1)
    end

    # We need to ensure that provided group exist
    group = Rubix.connection.request('hostgroup.get', 'filter' => { 'name' => cnf[key]['group'] })
    if group.has_data?
      group = group.result.reduce Hash.new, :merge
    else
      log.error "Host group #{cnf[key]['group']} doesn't exist !"
      exit(-1)
    end

    # Get hosts list
    hosts = Rubix.connection.request('host.get', 'groupids' => group["groupid"], 'proxyids' => proxy["proxyid"] )
    hosts = hosts.result
    if hosts.count.to_i.zero?
      log.error "No hosts to update on this group"
      exit(-1)
    else
      log.info "We need to update #{hosts.count} hosts"
    end

    count = 0
    x = eval(cnf[key]["rules"])
    RULES = Hash[x.map {|template, regexp| [template, regexp]} ]
    while count < hosts.count  do
      host_id = hosts.fetch(count)["hostid"]
      host = Rubix.connection.request('host.getobjects', 'hostid'=> host_id )
      sys_information = Array.new

      # Check if host is up
      if Net::Ping::External.new(host.result[0]["host"]).ping?
        SNMP::Manager.open(:host => host.result[0]["host"], :community => cnf[key]['snmp_community'], :version => :SNMPv2c) do |manager|
          begin
            response = manager.get(["sysDescr.0","sysName.0"])
            response.each_varbind do |vb|
              sys_information.push(vb.value.to_s)
            end
          rescue
            log.error "SNMP request timeout. Host #{host.result[0]["host"]} is not responding."
          end
        end
        if sys_information.any?
          RULES.each { |k,v|

            # We can update device if we match the template regexp
            if sys_information[0].to_s.match("#{v}")

              # Lookup for the template ID
              template = Rubix.connection.request('template.get', 'filter' => {'name' => k })
              if template.has_data?

                # We need to save templateid
                templateid = template[0]["templateid"]
                sys_information = sys_information[1].split('.')
                sys_information = sys_information[0].upcase

                # We can update the host
                update = Rubix.connection.request('host.update', 'hostid'=>host_id,'name'=>sys_information,"status" => 0, "inventory_mode"=>1,"macros"=>[{"macro"=>"{$SNMP_COMMUNITY}","value"=> cnf[key]['snmp_community']}], "templates" => [{"templateid"=>templateid}])
                if update.success?
                  log.info "Device #{sys_information} updated (ID#{host_id})."
                else
                  log.warn "Device #{sys_information} not updated (ID#{host_id})."
                end
              else
                log.error "Template #{k} doesn't exist !"
              end
            end
          }
        else
          log.error "No response receive from #{host.result[0]["host"]} !"
        end
      else
        log.error "Host #{host.result[0]["host"]} is not responding."
      end
      count = count +1
    end
  end
}
